{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "wooden-toner",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 5.41 ms (started: 2021-08-07 23:18:30 +08:00)\n"
     ]
    }
   ],
   "source": [
    "# 自动计算cell的计算时间\n",
    "%load_ext autotime\n",
    "\n",
    "%config InlineBackend.figure_format='svg' #矢量图设置，让绘图更清晰"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "automatic-navigation",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "origin\tgit@github.com:ustchope/dlwpn.git (fetch)\n",
      "origin\tgit@github.com:ustchope/dlwpn.git (push)\n",
      "[master 30fd126] 更新 ch13 #3 change Aug 08, 2021\n",
      " 1 file changed, 539 insertions(+), 100 deletions(-)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "To github.com:ustchope/dlwpn.git\n",
      "   55c24be..30fd126  master -> master\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.65 s (started: 2021-08-08 21:28:25 +08:00)\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "# 增加更新\n",
    "git add *.ipynb\n",
    "\n",
    "git remote -v\n",
    "\n",
    "git commit -m '更新 ch13 #3 change Aug 08, 2021'\n",
    "\n",
    "git push origin master"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "choice-stability",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.64 s (started: 2021-08-07 23:19:44 +08:00)\n"
     ]
    }
   ],
   "source": [
    "#设置使用的gpu\n",
    "import tensorflow as tf\n",
    "\n",
    "gpus = tf.config.list_physical_devices(\"GPU\")\n",
    "\n",
    "if gpus:\n",
    "   \n",
    "    gpu0 = gpus[0] #如果有多个GPU，仅使用第0个GPU\n",
    "    tf.config.experimental.set_memory_growth(gpu0, True) #设置GPU显存用量按需使用\n",
    "    # 或者也可以设置GPU显存为固定使用量(例如：4G)\n",
    "    #tf.config.experimental.set_virtual_device_configuration(gpu0,\n",
    "    #    [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=4096)]) \n",
    "    tf.config.set_visible_devices([gpu0],\"GPU\") "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "defensive-liver",
   "metadata": {},
   "source": [
    "# 现实世界的最佳实践"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "identified-fishing",
   "metadata": {},
   "source": [
    "**本章内容包含** \n",
    "\n",
    "* 超参数调优\n",
    "* 模型集成\n",
    "* 混合精度训练\n",
    "* 在多个 GPU 或 TPU 上训练 Keras 模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "vital-declaration",
   "metadata": {},
   "source": [
    "从这本书开始，你已经走了很远。您现在可以训练图像分类模型、图像分割模型、矢量数据分类或回归模型、时间序列预测模型、文本分类模型、序列到序列模型，甚至文本和图像的生成模型。你已经涵盖了所有的基础。\n",
    "\n",
    "但是，到目前为止，您的模型都经过小规模训练——使用单个 GPU 在小数据集上进行——并且它们通常还没有在我们查看的每个数据集上达到可实现的最佳性能。毕竟，这本书是一本入门书。如果要走出现实世界并在全新问题上取得最先进的结果，您仍然需要跨越一些鸿沟。\n",
    "\n",
    "倒数第二章是关于弥合这一差距，并为您提供从机器学习学生到成熟的机器学习工程师所需的最佳实践。我们将回顾系统地提高模型性能的基本技术：超参数调整和模型集成。然后，我们将研究如何通过多 GPU 和 TPU 训练、混合精度以及利用云中的远程计算资源来加速和扩大模型训练。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "controversial-knife",
   "metadata": {},
   "source": [
    "## 充分利用您的模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "similar-burner",
   "metadata": {},
   "source": [
    "如果你只需要一些可以正常工作的东西，盲目地尝试不同的架构配置就足够了。 在本节中，我们将超越“工作正常”，到“工作出色并赢得机器学习竞赛”，通过快速指南，了解一组用于构建最先进深度学习的必备技术 楷模。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "indian-visiting",
   "metadata": {},
   "source": [
    "### 超参数优化"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "impressive-deposit",
   "metadata": {},
   "source": [
    "在构建深度学习模型时，您必须做出许多看似随意的决定：您应该堆叠多少层？ 每层应该有多少个单元或过滤器？ 你应该使用 relu 作为激活，还是不同的功能？ 您应该在给定层之后使用 BatchNormalization 吗？ 你应该使用多少dropout？ 等等。 这些架构级参数称为超参数，以将它们与模型的参数区分开来，这些参数是通过反向传播训练的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dominican-pasta",
   "metadata": {},
   "source": [
    "在实践中，经验丰富的机器学习工程师和研究人员会随着时间的推移对这些选择中哪些有效哪些无效建立直觉——他们开发了超参数调整技能。 但是没有正式的规则。 如果您想达到在给定任务上所能达到的极限，您不能满足于这样任意的选择。 即使您有很好的直觉，您最初的决定也几乎总是次优的。 你可以通过手动调整和反复重新训练模型来完善你的选择——这是机器学习工程师和研究人员花费大部分时间做的事情。 但是，作为人类，整天摆弄超参数不应该是你的工作——最好让机器来处理。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "statutory-apache",
   "metadata": {},
   "source": [
    "因此，您需要以有原则的方式自动、系统地探索可能的决策空间。 您需要搜索架构空间并根据经验找到性能最佳的架构。 这就是自动超参数优化领域的意义所在：它是一个完整的研究领域，也是一个重要的领域。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "public-chinese",
   "metadata": {},
   "source": [
    "优化超参数的过程通常如下所示：\n",
    "* 选择一组超参数（自动）。\n",
    "* 建立相应的模型。\n",
    "* 将其拟合到您的训练数据，并测量验证数据的性能。\n",
    "* 选择下一组要尝试的超参数（自动）。\n",
    "* 重复。\n",
    "* 最后，测量测试数据的性能。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "experimental-italic",
   "metadata": {},
   "source": [
    "这个过程的关键是分析验证性能和各种超参数值之间关系的算法，以选择下一组要评估的超参数。 许多不同的技术都是可能的：贝叶斯优化、遗传算法、简单随机搜索等。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "present-bradley",
   "metadata": {},
   "source": [
    "训练模型的权重相对容易：您在小批量数据上计算损失函数，然后使用反向传播将权重向正确的方向移动。另一方面，更新超参数提出了独特的挑战。考虑一下：\n",
    "* 超参数空间通常由离散决策组成，因此不是连续或可微的。因此，您通常无法在超参数空间中进行梯度下降。相反，您必须依赖无梯度优化技术，其效率自然远低于梯度下降。\n",
    "* 计算这个优化过程的反馈信号（这组超参数是否会导致这个任务的高性能模型？）可能非常昂贵：它需要在数据集上从头开始创建和训练一个新模型。\n",
    "* 反馈信号可能是嘈杂的：如果训练运行的性能提高 0.2%，这是因为更好的模型配置，还是因为您对初始权重值很幸运？"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "optical-weekend",
   "metadata": {},
   "source": [
    "幸运的是，有一个工具可以让超参数调整变得更简单：KerasTuner。 让我们来看看。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "alert-punch",
   "metadata": {},
   "source": [
    "**使用 KERASTUNER**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "extended-sensitivity",
   "metadata": {},
   "source": [
    "让我们从安装 KerasTuner 开始："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "ideal-phrase",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[33mWARNING: Ignoring invalid distribution -rpcio (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -f-nightly-gpu (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -rpcio (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -f-nightly-gpu (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "Looking in indexes: https://pypi.douban.com/simple\n",
      "Collecting git+https://github.com/keras-team/keras-tuner.git\n",
      "  Cloning https://github.com/keras-team/keras-tuner.git to /tmp/pip-req-build-bv6rpfep\n",
      "  Running command git clone -q https://github.com/keras-team/keras-tuner.git /tmp/pip-req-build-bv6rpfep\n",
      "Requirement already satisfied: packaging in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (20.9)\n",
      "Requirement already satisfied: numpy in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (1.19.5)\n",
      "Requirement already satisfied: requests in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (2.25.1)\n",
      "Requirement already satisfied: scipy in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (1.6.2)\n",
      "Requirement already satisfied: tensorboard in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (2.5.0)\n",
      "Requirement already satisfied: ipython in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (7.23.1)\n",
      "Requirement already satisfied: kt-legacy in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from keras-tuner==1.0.3) (1.0.3)\n",
      "Requirement already satisfied: pickleshare in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (0.7.5)\n",
      "Requirement already satisfied: backcall in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (0.2.0)\n",
      "Requirement already satisfied: matplotlib-inline in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (0.1.2)\n",
      "Requirement already satisfied: pexpect>4.3 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (4.8.0)\n",
      "Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (3.0.18)\n",
      "Requirement already satisfied: setuptools>=18.5 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (57.0.0)\n",
      "Requirement already satisfied: traitlets>=4.2 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (5.0.5)\n",
      "Requirement already satisfied: pygments in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (2.9.0)\n",
      "Requirement already satisfied: decorator in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (4.4.2)\n",
      "Requirement already satisfied: jedi>=0.16 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from ipython->keras-tuner==1.0.3) (0.18.0)\n",
      "Requirement already satisfied: parso<0.9.0,>=0.8.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from jedi>=0.16->ipython->keras-tuner==1.0.3) (0.8.2)\n",
      "Requirement already satisfied: ptyprocess>=0.5 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from pexpect>4.3->ipython->keras-tuner==1.0.3) (0.7.0)\n",
      "Requirement already satisfied: wcwidth in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython->keras-tuner==1.0.3) (0.2.5)\n",
      "Requirement already satisfied: ipython-genutils in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from traitlets>=4.2->ipython->keras-tuner==1.0.3) (0.2.0)\n",
      "Requirement already satisfied: pyparsing>=2.0.2 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from packaging->keras-tuner==1.0.3) (2.4.7)\n",
      "Requirement already satisfied: idna<3,>=2.5 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from requests->keras-tuner==1.0.3) (2.10)\n",
      "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from requests->keras-tuner==1.0.3) (1.26.4)\n",
      "Requirement already satisfied: certifi>=2017.4.17 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from requests->keras-tuner==1.0.3) (2021.5.30)\n",
      "Requirement already satisfied: chardet<5,>=3.0.2 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from requests->keras-tuner==1.0.3) (3.0.4)\n",
      "Requirement already satisfied: wheel>=0.26 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (0.36.2)\n",
      "Requirement already satisfied: markdown>=2.6.8 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (3.3.4)\n",
      "Requirement already satisfied: grpcio>=1.24.3 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (1.34.1)\n",
      "Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (1.6.0)\n",
      "Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (0.4.4)\n",
      "Requirement already satisfied: google-auth<2,>=1.6.3 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (1.30.0)\n",
      "Requirement already satisfied: protobuf>=3.6.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (3.14.0)\n",
      "Requirement already satisfied: tensorboard-data-server<0.7.0,>=0.6.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (0.6.1)\n",
      "Requirement already satisfied: werkzeug>=0.11.15 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (1.0.1)\n",
      "Requirement already satisfied: absl-py>=0.4 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from tensorboard->keras-tuner==1.0.3) (0.13.0)\n",
      "Requirement already satisfied: six in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from absl-py>=0.4->tensorboard->keras-tuner==1.0.3) (1.15.0)\n",
      "Requirement already satisfied: rsa<5,>=3.1.4 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from google-auth<2,>=1.6.3->tensorboard->keras-tuner==1.0.3) (4.7.2)\n",
      "Requirement already satisfied: cachetools<5.0,>=2.0.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from google-auth<2,>=1.6.3->tensorboard->keras-tuner==1.0.3) (4.2.2)\n",
      "Requirement already satisfied: pyasn1-modules>=0.2.1 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from google-auth<2,>=1.6.3->tensorboard->keras-tuner==1.0.3) (0.2.8)\n",
      "Requirement already satisfied: requests-oauthlib>=0.7.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard->keras-tuner==1.0.3) (1.3.0)\n",
      "Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from pyasn1-modules>=0.2.1->google-auth<2,>=1.6.3->tensorboard->keras-tuner==1.0.3) (0.4.8)\n",
      "Requirement already satisfied: oauthlib>=3.0.0 in /public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard->keras-tuner==1.0.3) (3.1.0)\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -rpcio (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -f-nightly-gpu (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -rpcio (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -f-nightly-gpu (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -rpcio (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "\u001b[33mWARNING: Ignoring invalid distribution -f-nightly-gpu (/public/huangwei/miniconda3/envs/tensorflow/lib/python3.8/site-packages)\u001b[0m\n",
      "Note: you may need to restart the kernel to use updated packages.\n",
      "time: 13.1 s (started: 2021-08-08 00:12:43 +08:00)\n"
     ]
    }
   ],
   "source": [
    "pip install git+https://github.com/keras-team/keras-tuner.git"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "furnished-metabolism",
   "metadata": {},
   "source": [
    "KerasTuner 的核心思想是让你用一系列可能的选择来替换硬编码的超参数值，如units=32，如 Int(name=\"units\", min_value=16, max_value=64, step=16）。 给定模型中的一组此类选择称为超参数调整过程的搜索空间。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sought-thanksgiving",
   "metadata": {},
   "source": [
    "要指定搜索空间，请定义模型构建函数（参见代码清单 13.1）。 它接受一个 hp 参数，您可以从中采样超参数范围，并返回一个编译的 Keras 模型。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "earned-island",
   "metadata": {},
   "source": [
    "> 清单 13.1 KerasTuner 模型构建函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "treated-electron",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.8 ms (started: 2021-08-07 23:35:40 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow import keras\n",
    "from tensorflow.keras import layers\n",
    "\n",
    "def build_model(hp):\n",
    "#     来自 hp 对象的样本超参数值。 采样后，这些值（例如这里的“单位”变量）只是常规的 Python 常量。\n",
    "    units = hp.Int(name=\"units\", min_value=16, max_value=64, step=16) \n",
    "    model = keras.Sequential([layers.Dense(units, activation=\"relu\"),\n",
    "                              layers.Dense(10, activation=\"softmax\")])\n",
    "    \n",
    "#     可以使用不同种类的超参数：Int、Float、Boolean、Choice。\n",
    "    optimizer = hp.Choice(name=\"optimizer\", values=[\"rmsprop\", \"adam\"]) \n",
    "    \n",
    "    model.compile(optimizer=optimizer,\n",
    "                  loss=\"sparse_categorical_crossentropy\",\n",
    "                  metrics=[\"accuracy\"])\n",
    "    # 该函数返回一个编译模型。\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "altered-order",
   "metadata": {},
   "source": [
    "如果你想采用更加模块化和可配置的模型构建方法，你也可以继承 HyperModel 类并定义一个build()方法，如下所示："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "favorite-sociology",
   "metadata": {},
   "source": [
    "> 清单 13.2 KerasTuner 超模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "superior-intelligence",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.03 ms (started: 2021-08-08 00:13:59 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import keras_tuner as kt\n",
    "\n",
    "class SimpleMLP(kt.HyperModel):\n",
    "    def __init__(self, num_classes): \n",
    "        self.num_classes = num_classes\n",
    "\n",
    "#     该方法与我们之前构建的 build_model 独立函数相同。\n",
    "    def build(self, hp): \n",
    "        units = hp.Int(name=\"units\", min_value=16, max_value=64, step=16)\n",
    "        \n",
    "        # 由于面向对象的方法，我们可以将模型常量配置为构造函数参数（而不是在模型构建函数中对其进行硬编码）。\n",
    "        model = keras.Sequential([layers.Dense(units, activation=\"relu\"),\n",
    "                                  layers.Dense(self.num_classes, activation=\"softmax\") ])\n",
    "        \n",
    "        optimizer = hp.Choice(name=\"optimizer\", values=[\"rmsprop\", \"adam\"])\n",
    "        model.compile(optimizer=optimizer,\n",
    "                      loss=\"sparse_categorical_crossentropy\",\n",
    "                      metrics=[\"accuracy\"])\n",
    "        return model\n",
    "\n",
    "hypermodel = SimpleMLP(num_classes=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "right-society",
   "metadata": {},
   "source": [
    "下一步是定义一个“调谐器”。 从原理上讲，您可以将调谐器视为循环，它会重复执行：\n",
    "* 选择一组超参数值。\n",
    "* 使用这些值调用模型构建函数以创建模型。\n",
    "* 训练模型并记录其指标。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "preliminary-compensation",
   "metadata": {},
   "source": [
    "KerasTuner 有几个可用的内置调谐器 - RandomSearch，BayesianOptimization ，Hyperband 。 让我们尝试一下 BayesianOptimization，这是一个调谐器，它试图做出智能预测，根据先前选择的结果，新的超参数值可能表现最佳。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "appointed-limitation",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.37 s (started: 2021-08-08 00:23:47 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tuner = kt.BayesianOptimization(\n",
    "#     指定模型构建函数（或超模型实例）\n",
    "    build_model, \n",
    "    # 指定调谐器将寻求优化的指标。 始终指定验证指标，因为搜索过程的目标是找到泛化模型！\n",
    "    objective=\"val_accuracy\", \n",
    "    # 在结束搜索之前要尝试的不同模型配置（“试验”）的最大数量。\n",
    "    max_trials=100, \n",
    "    # 为了减少指标差异，您可以多次训练同一个模型并平均结果。 \n",
    "#     executions_per_trial 是每个模型配置（试验）运行的训练轮次（执行）的数量。\n",
    "    executions_per_trial=2, \n",
    "#   搜索日志的存储位置。\n",
    "    directory=\"mnist_kt_test\", \n",
    "    # 是否覆盖数据以开始新的搜索。 如果您修改了模型构建功能的目录，\n",
    "#     则将此设置为 True，或者将其设置为 False 以使用相同的模型构建功能恢复先前开始的搜索。\n",
    "    overwrite=True, \n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "crude-bosnia",
   "metadata": {},
   "source": [
    "您可以通过 search_space_summary() 显示搜索空间的概览："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "earlier-pride",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Search space summary\n",
      "Default search space size: 2\n",
      "units (Int)\n",
      "{'default': None, 'conditions': [], 'min_value': 16, 'max_value': 64, 'step': 16, 'sampling': None}\n",
      "optimizer (Choice)\n",
      "{'default': 'rmsprop', 'conditions': [], 'values': ['rmsprop', 'adam'], 'ordered': False}\n",
      "time: 834 µs (started: 2021-08-08 00:24:44 +08:00)\n"
     ]
    }
   ],
   "source": [
    "tuner.search_space_summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "future-graphic",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['BayesianOptimization',\n",
       " 'Hyperband',\n",
       " 'RandomSearch',\n",
       " 'Sklearn',\n",
       " 'SklearnTuner',\n",
       " '__builtins__',\n",
       " '__cached__',\n",
       " '__doc__',\n",
       " '__file__',\n",
       " '__loader__',\n",
       " '__name__',\n",
       " '__package__',\n",
       " '__path__',\n",
       " '__spec__',\n",
       " 'absolute_import',\n",
       " 'bayesian',\n",
       " 'hyperband',\n",
       " 'randomsearch',\n",
       " 'sklearn_tuner']"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.59 ms (started: 2021-08-08 00:31:44 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dir(kt.tuners)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "iraqi-basic",
   "metadata": {},
   "source": [
    "> **目标最大化和最小化**\n",
    ">\n",
    "> 对于内置指标（如在我们的例子中的准确性），度量方向的 的（应该最大化准确性，但应该最小化损失）由 KerasTuner 推断。 但是，对于自定义指标，您应该自己指定它，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fatal-current",
   "metadata": {},
   "outputs": [],
   "source": [
    "objective = kt.Objective(\n",
    "#     指标的名称，在周期日志中找到\n",
    "    name=\"val_accuracy\", \n",
    "    # 指标所需的方向：“min”或“max”\n",
    "    direction=\"max\") \n",
    "    tuner = kt.BayesianOptimization(\n",
    "    build_model,\n",
    "    objective=objective,\n",
    "    ...\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "protected-navigator",
   "metadata": {},
   "source": [
    "最后，让我们开始搜索。 不要忘记传递验证数据，并确保不要将您的测试集用作验证数据——否则您很快就会开始过度拟合您的测试数据，并且您将无法再信任您的测试指标。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "twenty-viking",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Trial 100 Complete [00h 01m 23s]\n",
      "val_accuracy: 0.9744000136852264\n",
      "\n",
      "Best val_accuracy So Far: 0.9759999811649323\n",
      "Total elapsed time: 02h 08m 02s\n",
      "INFO:tensorflow:Oracle triggered exit\n",
      "time: 2h 8min 2s (started: 2021-08-08 00:37:42 +08:00)\n"
     ]
    }
   ],
   "source": [
    "(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()\n",
    "x_train = x_train.reshape((-1, 28 * 28)).astype(\"float32\") / 255\n",
    "x_test = x_test.reshape((-1, 28 * 28)).astype(\"float32\") / 255\n",
    "\n",
    "# 保留这些以备后用\n",
    "x_train_full = x_train[:] \n",
    "y_train_full = y_train[:] \n",
    "\n",
    "\n",
    "# 留出一个验证集\n",
    "num_val_samples = 10000 \n",
    "x_train, x_val = x_train[:-num_val_samples], x_train[-num_val_samples:] \n",
    "y_train, y_val = y_train[:-num_val_samples], y_train[-num_val_samples:] \n",
    "\n",
    "# 使用大量 epoch（您事先不知道。每个模型需要多少个 epoch）\n",
    "# 并在开始过度拟合时使用 EarlyStopping 回调来停止训练。\n",
    "callbacks = [keras.callbacks.EarlyStopping(monitor=\"val_loss\", patience=5), ]\n",
    "\n",
    "# 这采用与 fit() 相同的参数（它简单地将它们传递给每个新模型的 fit() ）。\n",
    "tuner.search( \n",
    "    x_train, y_train,\n",
    "    batch_size=128,\n",
    "    epochs=100, \n",
    "    validation_data=(x_val, y_val),\n",
    "    callbacks=callbacks,\n",
    "    verbose=2,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "painful-twelve",
   "metadata": {},
   "source": [
    "上面的例子将在几分钟内运行，因为我们只考虑了几个可能的选择，而且我们正在 MNIST 上进行训练。 但是，对于典型的搜索空间和典型的数据集，您经常会发现自己让超参数搜索在一夜之间甚至几天内运行。 如果您的搜索过程崩溃，您可以随时重新启动它——只需在调谐器中指定 overwrite=False 以便它可以从存储在磁盘上的试验日志中恢复。\n",
    "\n",
    "搜索完成后，您可以查询最佳超参数配置，您可以使用这些配置创建高性能模型，然后重新训练："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "threaded-wrong",
   "metadata": {},
   "source": [
    "> 清单 13.3 查询最佳超参数配置"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "pursuant-toilet",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.42 ms (started: 2021-08-08 12:01:30 +08:00)\n"
     ]
    }
   ],
   "source": [
    "top_n = 4\n",
    "# 返回 HyperParameters 对象的列表，您可以将其传递给建模函数。\n",
    "best_hps = tuner.get_best_hyperparameters(top_n)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "described-helping",
   "metadata": {},
   "source": [
    "通常，在重新训练这些模型时，您可能希望将验证数据作为训练数据的一部分，因为您不会进行进一步的超参数更改，因此您将不再评估验证数据的性能。 在我们的示例中，我们将在原始 MNIST 训练数据的整体上训练这些最终模型，而不保留验证集。\n",
    "\n",
    "但是，在我们可以对完整的训练数据进行训练之前，我们需要确定最后一个参数：要训练的最佳 epoch 数。 通常，您希望训练新模型的时间比搜索期间更长：在 EarlyStopping 回调中使用积极的耐心值可以节省搜索期间的时间，但可能会导致模型欠拟合。 只需使用验证集即可找到最佳周期："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "selective-lucas",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 740 µs (started: 2021-08-08 12:01:35 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def get_best_epoch(hp):\n",
    "    model = build_model(hp)\n",
    "#     请注意非常高的耐心值。\n",
    "    callbacks=[keras.callbacks.EarlyStopping(monitor=\"val_loss\", mode=\"min\", patience=10)]\n",
    "    \n",
    "    history = model.fit(x_train, \n",
    "                        y_train,\n",
    "                        validation_data=(x_val, y_val),\n",
    "                        epochs=100,\n",
    "                        batch_size=128,\n",
    "                        callbacks=callbacks)\n",
    "    \n",
    "    val_loss_per_epoch = history.history[\"val_loss\"]\n",
    "    best_epoch = val_loss_per_epoch.index(min(val_loss_per_epoch)) + 1\n",
    "    print(f\"Best epoch: {best_epoch}\")\n",
    "    return best_epoch"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "english-saturday",
   "metadata": {},
   "source": [
    "最后，在整个数据集上训练比这个 epoch 计数长一点的时间，因为你正在训练更多的数据——在这种情况下多 20%："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "planned-european",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/100\n",
      "391/391 [==============================] - 3s 5ms/step - loss: 0.4135 - accuracy: 0.8890 - val_loss: 0.2443 - val_accuracy: 0.9342\n",
      "Epoch 2/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.2232 - accuracy: 0.9360 - val_loss: 0.1802 - val_accuracy: 0.9510\n",
      "Epoch 3/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.1740 - accuracy: 0.9504 - val_loss: 0.1676 - val_accuracy: 0.9513\n",
      "Epoch 4/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.1418 - accuracy: 0.9585 - val_loss: 0.1378 - val_accuracy: 0.9594\n",
      "Epoch 5/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.1192 - accuracy: 0.9660 - val_loss: 0.1221 - val_accuracy: 0.9649\n",
      "Epoch 6/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.1024 - accuracy: 0.9708 - val_loss: 0.1103 - val_accuracy: 0.9682\n",
      "Epoch 7/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0891 - accuracy: 0.9755 - val_loss: 0.1084 - val_accuracy: 0.9685\n",
      "Epoch 8/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0785 - accuracy: 0.9775 - val_loss: 0.1040 - val_accuracy: 0.9690\n",
      "Epoch 9/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0700 - accuracy: 0.9805 - val_loss: 0.1044 - val_accuracy: 0.9697\n",
      "Epoch 10/100\n",
      "391/391 [==============================] - 1s 4ms/step - loss: 0.0631 - accuracy: 0.9822 - val_loss: 0.0989 - val_accuracy: 0.9706\n",
      "Epoch 11/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.0574 - accuracy: 0.9838 - val_loss: 0.0928 - val_accuracy: 0.9729\n",
      "Epoch 12/100\n",
      "391/391 [==============================] - 1s 4ms/step - loss: 0.0520 - accuracy: 0.9855 - val_loss: 0.0922 - val_accuracy: 0.9735\n",
      "Epoch 13/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.0478 - accuracy: 0.9869 - val_loss: 0.0920 - val_accuracy: 0.9739\n",
      "Epoch 14/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0436 - accuracy: 0.9881 - val_loss: 0.0910 - val_accuracy: 0.9745\n",
      "Epoch 15/100\n",
      "391/391 [==============================] - 2s 6ms/step - loss: 0.0397 - accuracy: 0.9892 - val_loss: 0.0902 - val_accuracy: 0.9740\n",
      "Epoch 16/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0366 - accuracy: 0.9901 - val_loss: 0.0918 - val_accuracy: 0.9733\n",
      "Epoch 17/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.0342 - accuracy: 0.9909 - val_loss: 0.0910 - val_accuracy: 0.9746\n",
      "Epoch 18/100\n",
      "391/391 [==============================] - 1s 4ms/step - loss: 0.0311 - accuracy: 0.9921 - val_loss: 0.0992 - val_accuracy: 0.9735\n",
      "Epoch 19/100\n",
      "391/391 [==============================] - 2s 4ms/step - loss: 0.0292 - accuracy: 0.9922 - val_loss: 0.0964 - val_accuracy: 0.9741\n",
      "Epoch 20/100\n",
      "391/391 [==============================] - 1s 4ms/step - loss: 0.0267 - accuracy: 0.9929 - val_loss: 0.0954 - val_accuracy: 0.9753\n",
      "Epoch 21/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0246 - accuracy: 0.9939 - val_loss: 0.0927 - val_accuracy: 0.9749\n",
      "Epoch 22/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0223 - accuracy: 0.9944 - val_loss: 0.0948 - val_accuracy: 0.9749\n",
      "Epoch 23/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0208 - accuracy: 0.9951 - val_loss: 0.0979 - val_accuracy: 0.9748\n",
      "Epoch 24/100\n",
      "391/391 [==============================] - 2s 5ms/step - loss: 0.0191 - accuracy: 0.9956 - val_loss: 0.0952 - val_accuracy: 0.9759\n",
      "Epoch 25/100\n",
      "391/391 [==============================] - 1s 3ms/step - loss: 0.0179 - accuracy: 0.9958 - val_loss: 0.1040 - val_accuracy: 0.9738\n",
      "Best epoch: 15\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'model' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-30-c5716a073d6a>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      9\u001b[0m \u001b[0mbest_models\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     10\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhp\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mbest_hps\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 11\u001b[0;31m     \u001b[0mmodel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_best_trained_model\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhp\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     12\u001b[0m     \u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mevaluate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx_test\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my_test\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     13\u001b[0m     \u001b[0mbest_models\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;32m<ipython-input-30-c5716a073d6a>\u001b[0m in \u001b[0;36mget_best_trained_model\u001b[0;34m(hp)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_best_trained_model\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhp\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m     \u001b[0mbest_epoch\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_best_epoch\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhp\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m     model.fit(x_train_full, \n\u001b[0m\u001b[1;32m      4\u001b[0m               \u001b[0my_train_full\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m               \u001b[0mbatch_size\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m128\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mNameError\u001b[0m: name 'model' is not defined"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 44.8 s (started: 2021-08-08 12:10:02 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def get_best_trained_model(hp):\n",
    "    best_epoch = get_best_epoch(hp)\n",
    "    model.fit(x_train_full, \n",
    "              y_train_full,\n",
    "              batch_size=128, \n",
    "              epochs=int(best_epoch * 1.2))\n",
    "    return model\n",
    "\n",
    "best_models = []\n",
    "for hp in best_hps:\n",
    "    model = get_best_trained_model(hp)\n",
    "    model.evaluate(x_test, y_test)\n",
    "    best_models.append(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "underlying-dividend",
   "metadata": {},
   "source": [
    "请注意，如果您不担心性能稍差，您可以采取一条捷径：只需使用调谐器重新加载性能最佳的模型，并在超参数搜索期间保存最佳权重，而无需从头开始重新训练新模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eligible-cartoon",
   "metadata": {},
   "outputs": [],
   "source": [
    "best_models = tuner.get_best_models(top_n)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "floppy-leone",
   "metadata": {},
   "source": [
    "> 打造正确搜索空间的艺术在大规模进行自动超参数优化时要记住的一个重要问题是验证集过度拟合。 因为您正在根据使用验证数据计算的信号更新超参数，所以您可以在验证数据上有效地训练它们，因此它们会很快过拟合验证数据。 始终牢记这一点。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tough-policy",
   "metadata": {},
   "source": [
    "**打造正确搜索空间的艺术**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "behavioral-projection",
   "metadata": {},
   "source": [
    "总的来说，超参数优化是一项强大的技术，是在任何任务上获得最先进的模型或赢得机器学习竞赛的绝对必要条件。想一想：曾几何时，人们手工制作了进入浅层机器学习模型的特征。这是非常次优的。现在，深度学习使分层特征工程的任务自动化——特征是使用反馈信号学习的，而不是手动调整的，这就是它应该的方式。同样，您不应该手工制作模型架构；您应该以有原则的方式优化它们。\n",
    "\n",
    "然而，进行超参数调整并不能替代熟悉模型架构最佳实践：搜索空间随着选择的数量而组合增长，因此将所有内容都转换为超参数并让调整器对其进行分类的成本太高了。您需要聪明地设计正确的搜索空间。超参数调优是自动化，而不是魔术：您可以使用它来自动化原本需要手动运行的实验，但您仍然需要精心挑选有可能产生良好指标的实验配置。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "norwegian-envelope",
   "metadata": {},
   "source": [
    "好消息：通过利用超参数调整，您必须做出的配置决策从微观决策（我为这一层选择多少单元？）升级到更高级别的架构决策（我应该在整个模型中使用残差连接吗？ ）。 虽然微决策特定于某个模型和某个数据集，但更高级别的决策可以更好地跨不同任务和数据集泛化：例如，几乎每个图像分类问题都可以通过相同类型的搜索空间模板解决。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "charming-macro",
   "metadata": {},
   "source": [
    "遵循这一逻辑，KerasTuner 尝试提供与广泛类别的问题（例如图像分类）相关的预制搜索空间。 只需添加数据，运行搜索，即可获得一个非常好的模型。 您可以尝试超模型 kt.applications.HyperXception 和 kt.applications.HyperResNet，它们是 Keras 应用程序模型的有效可调版本。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "divine-abortion",
   "metadata": {},
   "source": [
    "**超参数调整的未来：自动化机器学习**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "developing-leonard",
   "metadata": {},
   "source": [
    "目前，作为深度学习工程师，您的大部分工作包括使用 Python 脚本处理数据，然后详细调整深度网络的架构和超参数以获得工作模型，甚至获得最先进的模型 模型，如果你有那么大的野心。 不用说，这不是最佳设置。 但是自动化可以提供帮助，它不会仅仅停留在超参数调整上。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "hidden-department",
   "metadata": {},
   "source": [
    "搜索一组可能的学习率或可能的层大小只是第一步。 我们也可以更加雄心勃勃，尝试从头开始生成模型架构本身，限制尽可能少：例如，通过强化学习或遗传算法。 未来，整个端到端 ML 管道将自动生成，而不是由工程师手工制作。 这称为自动机器学习或 AutoML\n",
    ". 您已经可以利用 AutoKeras 之类的库：解决基本的 ML 问题，而您几乎不需要参与。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "animal-avenue",
   "metadata": {},
   "source": [
    "今天，AutoML 仍处于早期阶段，不能扩展到大问题。 但是，当 AutoML 成熟到可以被广泛采用时，机器学习工程师的工作不会消失——而是工程师将向价值创造链的上游移动。 他们将开始在数据管理上投入更多精力，制定真正反映业务目标的复杂损失函数，并了解他们的模型如何影响部署他们的数字生态系统（例如，使用模型预测的用户） 并生成模型的训练数据）——目前只有最大的公司才能考虑的问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sharing-omega",
   "metadata": {},
   "source": [
    "始终着眼大局，专注于理解基本面，并记住高度专业化的单调乏味最终将被自动化。 将其视为礼物——为您的工作流程提高生产力——而不是对您自己的相关性构成威胁。 无休止地调整旋钮不应该是你的工作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bacterial-surveillance",
   "metadata": {},
   "source": [
    "### 模型集成"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "mounted-involvement",
   "metadata": {},
   "source": [
    "另一种获得最佳任务结果的强大技术是模型集成。 集成包括将一组不同模型的预测汇集在一起以产生更好的预测。 如果您查看机器学习竞赛，特别是 Kaggle 上的竞赛，您会发现获胜者使用了非常大的模型集合，这些模型不可避免地击败了任何单个模型，无论模型有多好。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "chubby-classification",
   "metadata": {},
   "source": [
    "集成依赖于这样一个假设，即独立训练的不同表现良好的模型可能出于不同的原因是好的：每个模型查看数据的稍微不同的方面来进行预测，获得部分“真相”但不是全部。你可能熟悉盲人与大象的古老寓言：一群盲人第一次遇到大象，并试图通过触摸它来了解大象是什么。每个人接触大象身体的不同部位——只是一个部位，比如躯干或腿。然后男人们互相描述大象是什么：“它像一条蛇”、“像一根柱子或一棵树”等等。盲人本质上是机器学习模型，试图理解训练数据的多样性，每个模型都从自己的角度出发，使用自己的假设（由模型的独特架构和独特的随机权重初始化提供）。他们每个人都获得了数据的一部分真相，但不是全部真相。通过汇集他们的观点，您可以获得对数据的更准确的描述。大象是多个部分的组合：没有一个盲人完全正确，但是，一起接受采访时，他们可以讲述一个相当准确的故事。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spatial-balance",
   "metadata": {},
   "source": [
    "我们以分类为例。 汇集一组分类器的预测（以集成分类器）的最简单方法是在推理时平均它们的预测："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "lyric-acquisition",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用四种不同的模型来计算初始预测。\n",
    "preds_a = model_a.predict(x_val)\n",
    "preds_b = model_b.predict(x_val)\n",
    "preds_c = model_c.predict(x_val)\n",
    "preds_d = model_d.predict(x_val)\n",
    "\n",
    "# 这个新的预测阵列应该比任何初始预测阵列更准确。\n",
    "final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "contrary-breeding",
   "metadata": {},
   "source": [
    "然而，这只有在分类器或多或少同样好时才有效。 如果其中一个明显比另一个差，则最终的预测可能不如该组中最好的分类器。 集成分类器的一个更聪明的方法是进行加权平均，其中权重是在验证数据上学习的——通常，更好的分类器被赋予更高的权重，而较差的分类器被赋予更低的权重。 要搜索一组好的集成权重，您可以使用随机搜索或简单的优化算法，例如 Nelder-Mead 算法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "running-nutrition",
   "metadata": {},
   "outputs": [],
   "source": [
    "preds_a = model_a.predict(x_val)\n",
    "preds_b = model_b.predict(x_val)\n",
    "preds_c = model_c.predict(x_val)\n",
    "preds_d = model_d.predict(x_val)\n",
    "# 假设这些权重 (0.5, 0.25, 0.1, 0.15) 是根据经验学习的。\n",
    "final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exciting-denmark",
   "metadata": {},
   "source": [
    "有许多可能的变体：例如，您可以对预测的指数进行平均。 通常，对验证数据优化权重的简单加权平均值提供了非常强大的基线。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "multiple-christianity",
   "metadata": {},
   "source": [
    "使集成工作的关键是分类器的集合。多样性就是力量。多样性 如果所有的盲人都只摸过大象的鼻子，他们会同意大象就像蛇，他们将永远不知道大象的真相。多样性是使集成工作的原因。在机器学习方面，如果您的所有模型都以相同的方式存在偏差，那么您的集成将保留相同的偏差。如果您的模型以不同的方式存在偏差，偏差将相互抵消，并且集成将更加稳健和准确。\n",
    "\n",
    "出于这个原因，您应该集成尽可能好同时尽可能不同的模型。这通常意味着使用非常不同的架构，甚至不同品牌的机器学习方法。在很大程度上不值得做的一件事是，从不同的随机初始化中将经过多次独立训练的同一个网络集成在一起。如果您的模型之间的唯一区别是它们的随机初始化和它们暴露于训练数据的顺序，那么您的集成将是低多样性的，并且仅比任何单个模型提供微小的改进。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "noted-trainer",
   "metadata": {},
   "source": [
    "我发现在实践中行之有效的一件事是使用基于树的方法（例如随机森林或梯度提升树）和深度神经网络的集合，但这并不适用于所有问题域。 2014 年，Andrei Kolev 和我在 Kaggle (www.kaggle.com/c/higgs-boson) 的希格斯玻色子衰变检测挑战中使用各种树模型和深度神经网络的集合获得了第四名。值得注意的是，集成中的一个模型源自与其他模型不同的方法（它是正则化的贪婪森林），并且得分明显低于其他模型。不出所料，它在整体中被赋予了很小的权重。但令我们惊讶的是，结果证明它大大提高了整体的集成度，因为它与其他模型大不相同：它提供了其他模型无法访问的信息。这正是集成的重点。重要的不是你最好的模型有多好；这是关于你的候选模型集的多样性。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "amended-rally",
   "metadata": {},
   "source": [
    "## 扩大模型训练"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "demonstrated-defense",
   "metadata": {},
   "source": [
    "回想我们在第 7 章中介绍的“进度循环”概念：您的想法的质量取决于它们经历了多少改进周期。 您对一个想法进行迭代的速度取决于您设置实验的速度、运行该实验的速度以及最终分析结果数据的能力。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "responsible-polymer",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gt8r350oksj31800sy414.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acceptable-steps",
   "metadata": {},
   "source": [
    "随着您对 Keras API 的专业知识的发展，您编写深度学习实验的速度将不再是这个进度周期的瓶颈。 下一个瓶颈将成为训练模型的速度。 快速的训练基础设施意味着您可以在 10、15 分钟内获得结果，因此，您每天可以进行数十次迭代。 更快的训练直接提高了深度学习解决方案的质量。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "continuous-masters",
   "metadata": {},
   "source": [
    "在本节中，您将了解可以更快地训练模型的三种方法：\n",
    "* 混合精度训练，您甚至可以使用单个 GPU。\n",
    "* 在多个 GPU 上训练。\n",
    "* TPU 培训。\n",
    "\n",
    "我们开始吧。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "basic-yesterday",
   "metadata": {},
   "source": [
    "### 以混合精度加速 GPU 上的训练"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "steady-anthony",
   "metadata": {},
   "source": [
    "如果我告诉你有一种简单的技术，你可以使用它来将几乎任何模型的训练速度提高 3 倍，而且基本上是免费的？ 这似乎太好了，但确实如此，然而，这样的伎俩确实存在。 这就是混合精度训练。 要理解它是如何工作的，首先，我们需要看看计算机科学中“精度”的概念。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "automatic-legislature",
   "metadata": {},
   "source": [
    "**了解浮点精度**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "conceptual-diagnosis",
   "metadata": {},
   "source": [
    "精度之于数字就像分辨率之于图像。 由于计算机只能处理 1 和 0，因此计算机看到的任何数字都必须编码为二进制字符串。 例如，您可能熟悉 uint8 整数，它是在 8 位上编码的整数：00000000 表示 uint8 中的 0，11111111 表示 255。要表示 255 以上的整数，您需要添加更多位 - 8 位是不够的 . 大多数整数存储在 32 位上，可以表示范围从 -2147483648 到 2147483647 的有符号整数。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "proud-theme",
   "metadata": {},
   "source": [
    "浮点数是一样的。 在数学中，实数形成一个连续的轴：任意两个数之间有无数个点。 您始终可以放大实数轴。 在计算机科学中，这不是真的：例如，3 到 4 之间只有有限数量的中间点。 多少？ 嗯，这取决于您使用的精度：用于存储数字的位数。 您只能放大到特定分辨率。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "postal-finland",
   "metadata": {},
   "source": [
    "您通常会使用三个级别的精度：\n",
    "* 半精度或 float16，其中数字以 16 位存储。\n",
    "* 单精度或 float32，其中数字以 32 位存储。\n",
    "* 双精度或 float64，其中数字以 64 位存储。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "digital-elizabeth",
   "metadata": {},
   "source": [
    "> 关于浮点编码的说明"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "clean-river",
   "metadata": {},
   "source": [
    "关于浮点数的一个违反直觉的事实是可表示的数字不是均匀分布的。 较大的数字具有较低的精度：对于任何 N，2N 和 2(N +1) 之间的可表示值数量与 1 和 2 之间的可表示值数量相同。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "invisible-ceramic",
   "metadata": {},
   "source": [
    "这是因为浮点数被编码为三部分——符号、有效值（称为“尾数”）和指数，格式如下：\n",
    "```\n",
    "{sign} * (2 ** ({exponent} - 127)) * 1.{尾数}\n",
    "```\n",
    "例如，这里是如何编码最接近 Pi 的最接近的 float32 值："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "amino-protocol",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gt9bzmgpnaj30zk0ouwgz.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "matched-reasoning",
   "metadata": {},
   "source": [
    "出于这个原因，将数字转换为其浮点表示时产生的数值误差可能会因所考虑的确切值而有很大差异，并且对于绝对值较大的数字，误差往往会变得更大。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exclusive-reading",
   "metadata": {},
   "source": [
    "考虑浮点数分辨率的方法是根据您能够安全处理的两个任意数字之间的最小距离。 在单精度下，大约是 1e-7。 在双精度中，大约是 1e-16。 在半精度下，它只有 1e-3。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "governing-knowing",
   "metadata": {},
   "source": [
    "到目前为止，您在本书中看到的每个模型都使用单精度数：将其状态存储为权重变量，并在 float32 输入上运行计算。这足以在不丢失任何信息的情况下运行模型的向前和向后传递——尤其是在涉及小梯度更新时。（回想一下，典型的学习率为 1e-3，看到权重更新为 1e-6 的数量级是很常见的）。 "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bottom-affair",
   "metadata": {},
   "source": [
    "您也可以使用 ，尽管这会很浪费——像矩阵 float64 乘法或加法这样的运算在双精度中要昂贵得多，所以你会做两倍的工作而没有明显的好处。 但是你不能用权重和 float16 计算做同样的事情：梯度下降过程不会顺利运行，因为我们无法表示大约 1e-5 或 1e-6 的小梯度更新。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "smart-contrast",
   "metadata": {},
   "source": [
    "但是，您可以使用混合方法：这就是混合精度的含义。 这个想法是在精度不成问题的地方利用 16 位计算，同时在其他地方使用 32 位值来保持数值稳定性。 Moderns GPU 和 TPU 具有专用硬件，与等效的 32 位操作相比，这些硬件可以更快地运行 16 位操作并使用更少的内存。 通过尽可能使用这些较低精度的操作，您可以显着提高在这些设备上的训练速度。 同时，通过以单精度维护模型的精度敏感部分，您可以获得这些好处，而不会显着影响模型质量。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "international-advantage",
   "metadata": {},
   "source": [
    "这些好处是可观的：在现代 NVIDIA GPU 上，混合精度可以将训练速度提高多达 3 倍。 在 TPU 上进行训练（我们将在稍后介绍的主题）上进行训练时也很有用，它可以将训练速度提高 60%。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "accomplished-arnold",
   "metadata": {},
   "source": [
    "> 当心 dtype 默认值"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "weighted-examination",
   "metadata": {},
   "source": [
    "单精度是整个 Keras 和 TensorFlow 的默认浮点类型：除非您另行指定，否则您创建的任何张量或变量都将在 float32 中。 但是，对于 NumPy 数组，默认值为 float64！\n",
    "\n",
    "将默认的 NumPy 数组转换为 TensorFlow 张量将导致 float64 张量，这可能不是您想要的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "bridal-allah",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tf.float64"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.94 ms (started: 2021-08-08 14:02:59 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "import numpy as np\n",
    "\n",
    "np_array = np.zeros((2, 2))\n",
    "tf_tensor = tf.convert_to_tensor(np_array)\n",
    "tf_tensor.dtype"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "downtown-technique",
   "metadata": {},
   "source": [
    "记住在转换 NumPy 数组时明确数据类型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "regulation-consolidation",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tf.float32"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.22 ms (started: 2021-08-08 14:03:44 +08:00)\n"
     ]
    }
   ],
   "source": [
    "np_array = np.zeros((2, 2))\n",
    "tf_tensor = tf.convert_to_tensor(np_array, dtype=\"float32\") \n",
    "tf_tensor.dtype"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "extended-grave",
   "metadata": {},
   "source": [
    "请注意，当您使用 NumPy 数据调用 Keras fit() 方法时，它会为您执行此转换。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "immune-messenger",
   "metadata": {},
   "source": [
    "**混合精度训练实践**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "noted-bennett",
   "metadata": {},
   "source": [
    "在 GPU 上训练时，您可以像这样打开混合精度："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "renewable-individual",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:tensorflow:Mixed precision compatibility check (mixed_float16): OK\n",
      "Your GPUs will likely run quickly with dtype policy mixed_float16 as they all have compute capability of at least 7.0\n",
      "time: 9.34 ms (started: 2021-08-08 20:44:53 +08:00)\n"
     ]
    }
   ],
   "source": [
    "from tensorflow import keras\n",
    "keras.mixed_precision.set_global_policy(\"mixed_float16\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "different-reminder",
   "metadata": {},
   "source": [
    "通常，模型的大部分前向传递将在 float16 中完成（除了数值不稳定的操作，如 softmax），而模型的权重将在 float32 中存储和更新。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fiscal-groove",
   "metadata": {},
   "source": [
    "Keras 层有一个 variable_dtype 和一个 compute_dtype 属性。 默认情况下，这两个都设置为 float32。 当您打开混合精度时，大多数层的 compute_dtype 切换为 float16。 因此，这些层会将它们的输入转换为 float16，并将在 float16 中执行计算（使用权重的半精度副本）。 然而，由于它们的 variable_dtype 仍然是 float32，它们的权重将能够从优化器接收准确的 float32 更新，而不是半精度更新。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "impressive-conditioning",
   "metadata": {},
   "source": [
    "请注意，某些操作在 float16 中可能在数值上不稳定（特别是 softmax 和交叉熵）。 如果您需要为特定层选择退出混合精度，只需将参数 dtype=\"float32\" 传递给该层的构造函数。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "rotary-repair",
   "metadata": {},
   "source": [
    "### 多GPU训练"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "sexual-craft",
   "metadata": {},
   "source": [
    "虽然 GPU 每年都变得越来越强大，但深度学习模型也越来越大，需要更多的计算资源。 在单个 GPU 上进行训练对您的移动速度有严格的限制。 解决方案？ 您可以简单地添加更多 GPU，然后开始进行多 GPU 分布式训练。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "southern-affair",
   "metadata": {},
   "source": [
    "有两种方法可以跨多个设备分配计算：数据并行和模型并行。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "robust-blair",
   "metadata": {},
   "source": [
    "通过数据并行，单个模型可以在多台设备或多台机器上复制。 每个模型副本处理不同批次的数据，然后合并它们的结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "minute-terrain",
   "metadata": {},
   "source": [
    "通过模型并行，单个模型的不同部分在不同设备上运行，同时处理一批数据。 这最适用于具有自然并行架构的模型，例如具有多个分支的模型。 \n",
    "\n",
    "在实践中，模型并行仅用于模型太大而无法在任何单个设备上安装的情况：它不是用来加速常规模型的训练，而是作为训练更大模型的一种方式。 我们不会在这些页面中介绍模型并行性，我们将专注于您大部分时间使用的内容：数据并行性。 让我们来看看它是如何工作的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "intellectual-water",
   "metadata": {},
   "source": [
    "**使用两个或多个 GPU**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "accepted-reproduction",
   "metadata": {},
   "source": [
    "首先，您需要访问多个 GPU。 截至目前，Google Colab 仅允许您使用单个 GPU，因此您需要执行以下两项操作之一：\n",
    "\n",
    "* 获得 2-4 个 GPU，将它们安装在一台机器上（需要强大的电源），安装 CUDA 驱动程序、cuDNN 等。对于大多数人来说，这不是最佳选择。\n",
    "* 在 Google Cloud、Azure 或 AWS 上租用多 GPU 虚拟机 (VM)。 您将能够使用预装驱动程序和软件的 VM 映像，并且您的设置开销非常小。 对于不是 24/7 全天候训练模型的人来说，这可能是最佳选择。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "boolean-gardening",
   "metadata": {},
   "source": [
    "我们不会详细介绍如何启动多 GPU 云虚拟机，因为这些指令的生命周期相对较短，而且这些信息很容易在线获得。 如果你不想处理管理自己的 VM 实例的开销，你可以使用 TensorFlow Cloud: ，这是我和我的团队最近发布的一个包——它使你能够在多个 GPU 上开始训练，只需 在 Colab 笔记本的开头添加一行代码。 如果您正在寻找从在 Colab 中调试模型到尽可能快地在尽可能多的 GPU 上训练模型的无缝过渡，请查看它。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eleven-mailman",
   "metadata": {},
   "source": [
    "**单主机、多设备同步训练**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "novel-clinic",
   "metadata": {},
   "source": [
    "一旦您能够在具有多个 GPU 的机器上导入 tensorflow，您就可以在几秒钟内训练分布式模型。 它是这样工作的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "global-powell",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 创建一个“分发策略”对象。 MirroredStrategy 应该是您的首选解决方案。\n",
    "strategy = tf.distribute.MirroredStrategy() \n",
    "\n",
    "print(f\"Number of devices: {strategy.num_replicas_in_sync}\")\n",
    "\n",
    "# 用它打开一个“战略范围”\n",
    "with strategy.scope(): \n",
    "    # 创建变量的所有内容都应在策略范围内。 一般来说，这只是模型构建和 compile()。\n",
    "    model = get_compiled_model() \n",
    "    \n",
    "    \n",
    "#在所有可用设备上训练模型\n",
    "model.fit(train_dataset,\n",
    "          epochs=100,\n",
    "          validation_data=val_dataset,\n",
    "          callbacks=callbacks)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "human-turning",
   "metadata": {},
   "source": [
    "这几行代码实现了最常见的训练设置：单主机、多设备同步训练，在 TensorFlow 中也称为“镜像分发策略”。 “单主机”意味着所考虑的不同 GPU 都在一台机器上（而不是由许多机器组成的集群，每个机器都有自己的 GPU，通过网络进行通信）。 “同步训练”意味着每个 GPU 模型副本的状态始终保持不变——有些分布式训练的变体并非如此。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "special-villa",
   "metadata": {},
   "source": [
    "当您打开 MirroredStrategy 范围并在其中构建模型时，MirroredStrategy 对象将在每个可用 GPU 上创建一个模型副本（副本）。 然后，训练的每一步都以下列方式展开（见图 13.3）：\n",
    "* 从数据集中抽取一批数据（称为全局批处理）。\n",
    "* 它被分成 4 个不同的子批次（称为本地批次）。 例如，如果全局批次有 512 个样本，则 4 个本地批次中的每一个将有 128 个样本。 因为您希望本地批处理足够大以保持 GPU 忙碌，所以全局批处理大小通常需要非常大。\n",
    "* 4 个副本中的每一个都在自己的设备上独立地处理一个本地批次：它们运行前向传递，然后是反向传递。 每个副本输出一个“权重增量”，描述在给定先前权重相对于本地批次模型损失的梯度的情况下，模型中每个权重变量的更新量。\n",
    "* 源自局部梯度的权重增量在 4 个副本之间有效合并，以获得应用于所有副本的全局增量。 因为这是在每一步结束时完成的，所以副本始终保持同步：它们的权重始终相等。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aquatic-institution",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gt9oqpgey3j31bu0s0jus.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "about-shanghai",
   "metadata": {},
   "source": [
    "> tf.data 性能提示\n",
    ">\n",
    "> 在进行分布式训练时，始终将您的数据作为tf.data.Dataset 对象以保证最佳性能（将数据作为 NumPy 数组传递也有效，因为它们通过 fit() 转换为 Dataset 对象）。 您还应该确保利用数据预取：在将数据集传递给 fit() 之前，调用 dataset.prefetch(buffer_size)。 如果您不确定要选择什么缓冲区大小，请尝试 dataset.prefetch(tf.data.AUTOTUNE) 选项，它将为您选择一个缓冲区大小。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "funky-studio",
   "metadata": {},
   "source": [
    "在理想情况下，在 N 个 GPU 上训练将导致 N 因子的加速。然而，在实践中，分布引入了一些开销——特别是，合并源自不同设备的权重增量需要一些时间。 您获得的有效加速是使用的 GPU 数量的函数：\n",
    "* 使用两个 GPU，加速保持接近 2 倍。\n",
    "* 使用四个，加速约为 3.8 倍。\n",
    "* 8 倍，大约是 7.3 倍。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "designed-coordination",
   "metadata": {},
   "source": [
    "这假设您使用足够大的全局批处理大小来保持每个 GPU 的满负荷使用。 如果您的批量大小太小，本地批量大小将不足以让您的 GPU 保持忙碌。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "informed-entertainment",
   "metadata": {},
   "source": [
    "### TPU训练"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "complete-robin",
   "metadata": {},
   "source": [
    "除了 GPU 之外，深度学习领域普遍存在一种趋势，即将工作流转移到专门为深度学习工作流设计的日益专业化的硬件上（这种单用途芯片被称为 ASIC，专用集成电路）。 各种大大小小的公司都在开发新芯片，但今天沿着这些方向最突出的努力是谷歌的张量处理单元 (TPU)，它可以在谷歌云和谷歌 Colab 上获得。\n",
    "\n",
    "TPU 训练确实涉及跳过一些障碍。 但值得付出额外的努力：TPU 真的非常快。 在 TPU V2 上训练通常比在 NVIDIA P100 GPU 上训练快 15 倍。 对于大多数模型，TPU 训练的成本效益最终平均比 GPU 训练高 3 倍。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "lucky-playback",
   "metadata": {},
   "source": [
    "**通过 GOOGLE COLAB 使用 TPU**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cathedral-sunday",
   "metadata": {},
   "source": [
    "您实际上可以在 Colab 中免费使用 8 核 TPU。 在 Colab 菜单的“运行时”选项卡下的“更改运行时类型”选项中，您会注意到除了“GPU”运行时之外，您还可以访问“TPU”运行时。\n",
    "\n",
    "当您使用 GPU 运行时，您的模型可以直接访问 GPU，而无需执行任何特殊操作。 这不适用于 TPU 运行时：在开始构建模型之前，您需要执行一个额外的步骤：您需要连接到 TPU 集群。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afraid-english",
   "metadata": {},
   "source": [
    "它是这样工作的："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "written-choice",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "\n",
    "tpu = tf.distribute.cluster_resolver.TPUClusterResolver()\n",
    "print(\"Device:\", tpu.master())\n",
    "tf.config.experimental_connect_to_cluster(tpu)\n",
    "tf.tpu.experimental.initialize_tpu_system(tpu)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "employed-explorer",
   "metadata": {},
   "source": [
    "你不必太担心它的作用——它只是将你的笔记本运行时连接到设备的一个小咒语。 芝麻开门。\n",
    "\n",
    "与多 GPU 训练的情况非常相似，使用 TPU 需要您打开一个分发策略范围——在本例中为 TPUStrategy 范围。 TPUStrategy 遵循与 MirroredStrategy 相同的分发模板——模型在每个 TPU 核心复制一次，并且副本保持同步。\n",
    "\n",
    "这是一个简单的例子："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "capital-visiting",
   "metadata": {},
   "source": [
    "> 清单 13.4 在 TPUStrategy 范围内构建模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "certain-controversy",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tensorflow import keras\n",
    "from tensorflow.keras import layers\n",
    "\n",
    "strategy = tf.distribute.TPUStrategy(tpu)\n",
    "print(f\"Number of replicas: {strategy.num_replicas_in_sync}\")\n",
    "\n",
    "def build_model(input_size):\n",
    "    inputs = keras.Input((input_size, input_size, 3))\n",
    "    x = keras.applications.resnet.preprocess_input(inputs)\n",
    "    x = keras.applications.resnet.ResNet50(weights=None, \n",
    "                                           include_top=False, \n",
    "                                           pooling=\"max\")(x)\n",
    "    outputs = layers.Dense(10, activation=\"softmax\")(x)\n",
    "    model = keras.Model(inputs, outputs)\n",
    "    model.compile(optimizer=\"rmsprop\",\n",
    "                  loss=\"sparse_categorical_crossentropy\",\n",
    "                  metrics=[\"accuracy\"])\n",
    "    return model\n",
    "\n",
    "with strategy.scope():\n",
    "    model = build_model(input_size=32)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "positive-albert",
   "metadata": {},
   "source": [
    "我们几乎准备好开始训练了。 现在，Colab 中的 TPU 有点奇怪：它是两个 VM 设置，这意味着托管笔记本运行时的 VM 与 TPU 所在的 VM 不同。因此，您将无法 从存储在本地磁盘（也就是说，在链接到托管笔记本的 VM 的磁盘上）上的文件进行训练。 TPU 运行时无法从那里读取。 您有两种数据加载选项：\n",
    "* 从存在于 VM 内存（而非磁盘）中的数据进行训练。 如果您的数据在 NumPy 数组中，这就是您已经在做的事情。\n",
    "* 将数据存储在 Google Cloud Storage (GCS) 存储桶中，并创建直接从存储桶读取数据的数据集，无需在本地下载。 TPU 运行时可以从 GCS 读取数据。 对于太大而无法完全保存在内存中的数据集，这是唯一的选择。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "chubby-brazil",
   "metadata": {},
   "source": [
    "在我们的例子中，让我们从内存中的 NumPy 数组进行训练：CIFAR10 数据集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "indian-garlic",
   "metadata": {},
   "outputs": [],
   "source": [
    "(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()\n",
    "\n",
    "# 请注意，TPU 训练与多 GPU 训练非常相似，需要大批量以确保设备得到充分利用。\n",
    "model.fit(x_train, y_train, batch_size=1024)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "chubby-newman",
   "metadata": {},
   "source": [
    "你会注意到第一个 epoch 需要一段时间才能开始——这是因为你的模型正在被编译成 TPU 可以执行的东西。 一旦这一步完成，训练本身就会飞快地进行。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "independent-shakespeare",
   "metadata": {},
   "source": [
    "> 注意 I/O 瓶颈\n",
    "> \n",
    ">由于 TPU 可以极快地处理批量数据，因此从 GCS 读取数据的速度很容易成为瓶颈。\n",
    "> * 如果您的数据集足够小，则应将其保存在 VM 的内存中。 您可以通过对数据集调用 dataset.cache() 来实现。 这样，数据只会从 GCS 读取一次。\n",
    "> * 如果您的数据集太大而无法放入内存，请确保将其存储为 TFRecord 文件——一种可以非常快速加载的高效二进制存储格式。 在 keras.io 上，您会找到一个代码示例，演示如何将数据格式化为 TFRecord 文件："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "incorrect-reviewer",
   "metadata": {},
   "source": [
    "**利用 Step Fusion 提高 TPU 利用率**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "revolutionary-purple",
   "metadata": {},
   "source": [
    "由于 TPU 具有大量可用计算能力，因此您需要使用非常大的批次进行训练以保持 TPU 内核忙碌。 对于小型模型，所需的批次大小可能会非常大——每批次超过 10,000 个样本。 在处理大量批次时，您应该确保相应地提高优化器的学习率：您将对权重进行更少的更新，但每次更新都会更准确（因为梯度是使用更多数据点计算的）， 因此，您应该在每次更新时将权重移动更大的幅度。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "endangered-satisfaction",
   "metadata": {},
   "source": [
    "但是，您可以利用一个简单的技巧来保持合理大小的批次，同时保持充分的 TPU 利用率：. 这个想法是在步骤融合每个 TPU 执行步骤期间运行多个训练步骤。 基本上，在 VM 的两次往返之间做更多的工作\n",
    "TPU 的内存。 为此，只需在 compile() 中指定 steps_per_execution 参数，例如，steps_per_execution=8 以在每次 TPU 执行期间运行 8 个训练步骤。 对于未充分利用 TPU 的小型模型，这可能会导致显着的加速。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "quiet-timothy",
   "metadata": {},
   "source": [
    "## 章节总结"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "personal-stanford",
   "metadata": {},
   "source": [
    "* 您可以利用超参数调整和 KerasTuner 来自动化寻找最佳模型配置的繁琐工作。 但要注意验证集过度拟合！\n",
    "* 多种模型的集合通常可以显着提高预测质量。\n",
    "* 您可以通过打开混合精度来加速 GPU 上的模型训练——您通常会在几乎免费的情况下获得不错的速度提升。\n",
    "* 为了进一步扩展您的工作流程，您可以使用 tf.distribute.MirroredStrategy API 在多个 GPU 上训练模型您甚至* 可以使用 TPUStrategy API 在 Google 的 TPU（可在 Colab 上提供）上进行训练。 如果您的模型很小，请确保利用步进融合（通过 compile(..., steps_per_execution=N) 参数）以充分利用 TPU 内核。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "oriental-restoration",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:tensorflow]",
   "language": "python",
   "name": "conda-env-tensorflow-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
